﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TasksModel
{
    /// <summary>
    /// All dates are in specified time zone.
    /// </summary>
    /// <remarks>All times are in UTC.</remarks>
    [System.Xml.Serialization.XmlRootAttribute(Namespace = "http://mp.tasks.com/2010/")]
    public class TaskRecurrence : IdentifiedObject
    {
        #region Properties

        public enum RecurrenceType
        {
            None = 0,
            /// <summary>
            /// Every couple of days.
            /// </summary>
            Daily,
            /// <summary>
            /// In specified days of every week.
            /// </summary>
            Weekly,
            /// <summary>
            /// Every specified day of each month.
            /// </summary>
            Monthly,
            /// <summary>
            /// On last day of each month.
            /// </summary>
            LastDayOfMonth,
            /// <summary>
            /// Every specified day of each year.
            /// </summary>
            Yearly
        }

        private RecurrenceType _type;

        /// <summary>
        /// Required.
        /// </summary>
        public RecurrenceType Type
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _type; }
            set
            {
                if (_type != value)
                {
                    _type = value;
                    NotifyPropertyChanged("Type");
                }
            }
        }

        private int _everyDays;

        /// <summary>
        /// Meaningful for Daily only. 1-7, 1=everyday
        /// </summary>
        public int EveryDays
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _everyDays; }
            set
            {
                if (_everyDays != value)
                {
                    if (value < 0)
                        throw new ArgumentOutOfRangeException();
                    _everyDays = value;
                    NotifyPropertyChanged("EveryDays");
                }
            }
        }

        private List<DayOfWeek> _weeklyDays;

        /// <summary>
        /// Non-empty if set. Meaningful for Weekly only. Maximum of 7 items.
        /// </summary>
        public List<DayOfWeek> WeeklyDays
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _weeklyDays; }
            set
            {
                if (_weeklyDays != value)
                {
                    _weeklyDays = value;
                    NotifyPropertyChanged("WeeklyDays");
                }
            }
        }

        private int _dayOfMonth;

        /// <summary>
        /// Meaningful for Monthly only. 0-31
        /// </summary>
        public int DayOfMonth
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _dayOfMonth; }
            set
            {
                if (_dayOfMonth != value)
                {
                    if (value < 0)
                        throw new ArgumentOutOfRangeException();
                    _dayOfMonth = value;
                    NotifyPropertyChanged("DayOfMonth");
                }
            }
        }

        private int _dayOfYear;

        /// <summary>
        /// Meaningful for Yearly only. 0-367
        /// </summary>
        public int DayOfYear
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _dayOfYear; }
            set
            {
                if (_dayOfYear != value)
                {
                    if (value < 0)
                        throw new ArgumentOutOfRangeException();
                    _dayOfYear = value;
                    NotifyPropertyChanged("DayOfYear");
                }
            }
        }

        private DateTime _beginFrom;

        /// <summary>
        /// Occurence start. In UTC.
        /// </summary>
        public DateTime BeginFrom
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _beginFrom; }
            set
            {
                if (_beginFrom != value)
                {
                    System.Diagnostics.Debug.Assert(value.Kind == DateTimeKind.Utc);
                    _beginFrom = value;
                    NotifyPropertyChanged("BeginFrom");
                }
            }
        }

        private DateTime? _endAt;

        /// <summary>
        /// Occurence end. In UTC.
        /// </summary>
        public DateTime? EndAt
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return _endAt; }
            set
            {
                if (_endAt != value)
                {
                    System.Diagnostics.Debug.Assert(!value.HasValue || value.Value.Kind == DateTimeKind.Utc);
                    _endAt = value;
                    NotifyPropertyChanged("EndAt");
                }
            }
        }

        private DateTime? _nextOccurence;

        /// <summary>
        /// Calculated. Not serialized nor persisted. DateTime.MaxValue if no next occurence will exist.
        /// In UTC.
        /// </summary>
        [System.Xml.Serialization.XmlIgnore]
        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
        public DateTime NextOccurence
        {
            [System.Diagnostics.DebuggerStepThrough]
            get
            {
                if (!_nextOccurence.HasValue)
                    throw new InvalidOperationException("Invoke CalculateNextOccurence first.");
                return _nextOccurence.Value;
            }
        }

        public string UriSchema
        {
            [System.Diagnostics.DebuggerStepThrough]
            get { return BaseSchema + "recurrence"; }
        }

        #endregion

        public TaskRecurrence()
            : this(Guid.NewGuid())
        {
        }

        public TaskRecurrence(Guid id)
        {
            ID = id;
        }

        public TaskRecurrence(TaskRecurrence tr)
            : this(tr.ID)
        {
            _beginFrom = tr._beginFrom;
            _dayOfMonth = tr._dayOfMonth;
            _dayOfYear = tr._dayOfYear;
            _endAt = tr._endAt;
            _everyDays = tr._everyDays;
            _nextOccurence = tr._nextOccurence;
            _type = tr._type;
            _weeklyDays = tr._weeklyDays;
            Timestamp = tr.Timestamp;
        }

        /// <summary>
        /// Required to use NextOccurence property.
        /// </summary>
        /// <param name="currentDateTime">current time in UTC</param>
        public void CalculateNextOccurence(DateTime currentDateTime)
        {
            System.Diagnostics.Debug.Assert(currentDateTime.Kind == DateTimeKind.Utc);
            if (currentDateTime < _beginFrom)
                currentDateTime = _beginFrom;
            DateTime next = _beginFrom;
            switch (_type)
            {
                case RecurrenceType.Daily:
                    while (next < currentDateTime)
                        next = next.AddDays(EveryDays);
                    break;
                case RecurrenceType.Weekly:
                    if (WeeklyDays == null || WeeklyDays.Count == 0)
                        next = currentDateTime;
                    else
                        while (next < currentDateTime || WeeklyDays.IndexOf(next.DayOfWeek) < 0)
                            next = next.AddDays(1);
                    break;
                case RecurrenceType.LastDayOfMonth:
                    int dim = DateTime.DaysInMonth(currentDateTime.Year, currentDateTime.Month);
                    next = currentDateTime.AddDays(dim - currentDateTime.Day);
                    break;
                case RecurrenceType.Monthly:
                    if (DayOfMonth > 0)
                    {
                        // cur 4.24 dom 4.23 => 5.23
                        // cur 4.24 dom 4.25 => 4.25
                        next = currentDateTime.AddDays(DayOfMonth - currentDateTime.Day);
                        if (currentDateTime.Day > DayOfMonth)
                            next = next.AddMonths(1);
                        System.Diagnostics.Debug.Assert(next.Day == DayOfMonth);
                    }
                    else
                        next = currentDateTime;
                    break;
                case RecurrenceType.Yearly:
                    if (DayOfYear > 0)
                    {
                        next = currentDateTime.AddDays(DayOfYear - currentDateTime.DayOfYear);
                        if (currentDateTime.DayOfYear > DayOfYear)
                            next = next.AddYears(1);
                        System.Diagnostics.Debug.Assert(next.DayOfYear == DayOfYear);
                    }
                    else
                        next = currentDateTime;
                    break;
                default:
                    throw new InvalidOperationException("Type value is invalid");
            }
            if (_endAt.HasValue && next > _endAt.Value)
                next = DateTime.MaxValue;
            _nextOccurence = new DateTime(next.Date.Ticks, DateTimeKind.Utc);
        }

        /// <summary>
        /// Get NextOccurence as in specified time zone.
        /// </summary>
        /// <param name="tzId">as for TimeZoneInfo.Id</param>
        /// <returns>time in specified time zone</returns>
        public DateTime LocalNextOccurence(string tzId)
        {
            return DateTimeTz.ToLocal(NextOccurence, tzId);
        }

        public DateTime LocalBeginFrom(string tzId)
        {
            return DateTimeTz.ToLocal(BeginFrom, tzId);
        }

        public DateTime? LocalEndAt(string tzId)
        {
            if (!EndAt.HasValue) return null;
            return DateTimeTz.ToLocal(EndAt.Value, tzId);
        }

        /// <summary>
        /// Return iCalendar compliant description.
        /// </summary>
        /// <returns></returns>
        public string ToICalendar()
        {
            //TODO
            //"DTSTART;VALUE=DATE:20070501\r\n" +
            //"DTEND;VALUE=DATE:20070502\r\n" +
            //"RRULE:FREQ=WEEKLY;BYDAY=Tu;UNTIL=20070904\r\n";
            return string.Empty;
        }
    }
}